import EventEmitter from 'events'
import type { Connect, ConnectEvents } from './index'
import { Actions } from '@/utils/action'
import type Store from '@/utils/store'

export class ConnectActionBase<C extends Connect = Connect> extends EventEmitter {
  constructor(name: string, connect: C) {
    super()
    this.name = name
    this.connect = connect
    this.store = connect.store
    this.bindHandlerThis()
    this.on('complete', () => this.reset())
    this.on('fail', () => this.reset())
  }

  private bindHandlerThis() {
    const prototype = Object.getPrototypeOf(this)
    const propertyNames = Object.getOwnPropertyNames(prototype) as (keyof this)[]

    for (const key of propertyNames) {
      const value = this[key]
      if (key.toString().endsWith('_handler') && typeof value === 'function') {
        this[key] = value.bind(this)
      }
    }
  }

  readonly name: string
  readonly connect: C
  readonly store: Store

  protected nextName?: string

  get logger() {
    return this.connect.logger
  }

  isRunning = false

  setNextName(name: string) {
    this.nextName = name
    return this
  }

  run() {
    return this
  }

  runPromise() {
    return Promise.resolve<any>(void 0)
  }

  private failInterceptorsFn: FailInterceptorsFn | null = null

  /**
   * 失败拦截器，用于在失败时修改错误信息
   */
  failInterceptors(fn: FailInterceptorsFn) {
    this.failInterceptorsFn = fn
    return this
  }

  protected fail(err?: Error | string) {
    let e: Error | string | false | undefined = err
    if (this.failInterceptorsFn) {
      e = this.failInterceptorsFn(err, this.complete.bind(this))
    }
    if (e !== false) {
      this.emit('fail', e)
    }
    return this
  }

  protected complete() {
    this.emit('complete', this.nextName)
    return this
  }

  protected packetOnce(listener: ConnectEvents['packet']) {
    this.connect.once('packet', listener)
    return () => this.connect.off('packet', listener)
  }

  protected reset() {
    this.isRunning = false
  }

  on<K extends keyof ConnectActionBaseEvents>(event: K, listener: ConnectActionBaseEvents[K]) {
    return super.on(event, listener)
  }

  once<K extends keyof ConnectActionBaseEvents>(event: K, listener: ConnectActionBaseEvents[K]) {
    return super.once(event, listener)
  }

  off<K extends keyof ConnectActionBaseEvents>(event: K, listener: ConnectActionBaseEvents[K]) {
    return super.off(event, listener)
  }

  emit<K extends keyof ConnectActionBaseEvents>(
    event: K,
    ...args: Parameters<ConnectActionBaseEvents[K]>
  ) {
    return super.emit(event, ...args)
  }
}

interface ConnectActionBaseEvents {
  complete: (name?: string) => void
  fail: (err?: Error | string) => void
}

export class ConnectAction<C extends Connect = Connect> extends ConnectActionBase<C> {
  readonly actions = new Actions()

  run() {
    if (this.isRunning) throw new Error('Action is already running')
    this.isRunning = true
    this.nextName = void 0
    try {
      this.actions.once('complete', () => this.complete()).run()
    } catch (error) {
      this.fail(error as any)
    }

    return this
  }

  async runPromise() {
    if (this.isRunning) throw new Error('Action is already running')
    this.isRunning = true
    this.nextName = void 0
    await this.actions.runPromise().catch((err) => {
      this.fail(err as any)
      return Promise.reject(err)
    })
    this.complete()
    return this.nextName
  }

  addAction(...args: Parameters<typeof this.actions.addAction>) {
    this.actions.addAction(...args)
    return this
  }

  addAsyncAction(...args: Parameters<typeof this.actions.addAsyncAction>) {
    this.actions.addAsyncAction(...args)
    return this
  }
}

export class ConnectActionGroup<
  C extends Connect = Connect,
  G extends Connect = Connect
> extends ConnectActionBase<C> {
  constructor(name: string, connect: C) {
    super(name, connect)
  }

  protected readonly actionGroup: (ConnectAction | ConnectActionGroup)[] = []

  protected index = 0

  isRunning = false

  addAction(action: ConnectAction<G> | ConnectActionGroup<G>) {
    if (this.actionGroup.some((item) => item.name === action.name)) {
      throw new Error(`Action ${action.name} already exists`)
    }
    this.actionGroup.push(action)
    return this
  }

  run() {
    if (this.isRunning) throw new Error('Action is already running')
    this.isRunning = true
    this.nextName = void 0
    return this._run()
  }

  runPromise() {
    return new Promise<string | undefined>((resolve, reject) => {
      if (this.isRunning) return reject(new Error('Action is already running'))
      this.isRunning = true
      this.nextName = void 0
      this.once('complete', resolve)
      this.once('fail', reject)
      this._runPromise()
    })
  }

  protected _run() {
    try {
      this.actionGroup[this.index]
        .once('complete', this.nextCallback.bind(this))
        .once('fail', this.fail.bind(this))
        .run()
    } catch (error) {
      this.fail(error as any)
    }
    return this
  }

  protected async _runPromise() {
    const name = await this.actionGroup[this.index].runPromise()
    this.nextCallbackPromise(name)
  }

  protected nextCallback(name?: string) {
    if (name) {
      const index = this.actionGroup.findIndex((action) => action.name === name)
      if (index === -1) {
        throw new Error(`Action ${name} not found`)
      }
      this.index = index
    } else {
      if (this.index >= this.actionGroup.length - 1) {
        return this.complete()
      } else {
        this.index++
      }
    }
    this._run()
  }

  protected nextCallbackPromise(name?: string) {
    if (name) {
      const index = this.actionGroup.findIndex((action) => action.name === name)
      if (index === -1) {
        throw new Error(`Action ${name} not found`)
      }
      this.index = index
    } else {
      if (this.index >= this.actionGroup.length - 1) {
        this.complete()
        return
      } else {
        this.index++
      }
    }
    this._runPromise()
  }

  protected reset() {
    this.index = 0
    super.reset()
  }
}

export type FailInterceptorsFn = (
  err: Error | string | undefined,
  complete: InstanceType<typeof ConnectActionBase>['complete']
) => Error | string | false | undefined
